﻿using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Windows.Controls;

namespace SearchScreen.SDK
{
    public class MetadataFactory
    {
        // DEBUG TEST
        public static string GenerateMetadataProxiesCode(SearchMetadata metadata)
        {
            CodeCompileUnit cu;
            CodeDomProvider provider;
            GenerateMetadataCodeCompileUnit(metadata, out cu, out provider);

            var sb = new StringBuilder();
            using (var wr = new StringWriter(sb))
            {
                var options = new CodeGeneratorOptions()
                {
                    BlankLinesBetweenMembers = true,
                    BracingStyle = "C",
                    ElseOnClosing = false,
                    IndentString = "\t",
                    VerbatimOrder = true
                };

                provider.GenerateCodeFromCompileUnit(cu, wr, options);

                wr.Flush();
            }
            return sb.ToString();
        }

        public static Assembly GenerateMetadataProxies(SearchMetadata metadata)
        {
            CodeCompileUnit cu;
            CodeDomProvider provider;
            GenerateMetadataCodeCompileUnit(metadata, out cu, out provider);

            var options = new CompilerParameters()
            {
                GenerateInMemory = true,
                IncludeDebugInformation = false,
                WarningLevel = 4
            };

            foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
            {
                if (assembly.ReflectionOnly || assembly.IsDynamic)
                    continue;

                var codebaseUri = new Uri(assembly.CodeBase);

                if (!File.Exists(codebaseUri.LocalPath))
                    continue;

                System.Diagnostics.Debug.WriteLine(string.Format("Added assembly reference {0}", codebaseUri.LocalPath));

                options.ReferencedAssemblies.Add(codebaseUri.LocalPath);
            }

            var results = provider.CompileAssemblyFromDom(options, cu);

            if (results.Errors.Count > 0)
            {
                throw new Exception(string.Join(Environment.NewLine,
                    results.Errors.OfType<CompilerError>().Select(e => e.ErrorText)));
            }

            return results.CompiledAssembly;
        }

        private static void GenerateMetadataCodeCompileUnit(SearchMetadata metadata, out CodeCompileUnit cu, out CodeDomProvider provider)
        {
            cu = new CodeCompileUnit();

            var ns = new CodeNamespace("GeneratedObjects");
            ns.Imports.Add(new CodeNamespaceImport("System"));

            cu.Namespaces.Add(ns);

            SortedDictionary<byte, string> groupList = new SortedDictionary<byte, string>();

            // Generate all the Groups
            foreach (var group in metadata.groups)
            {
                // CodeTypeDeclaration ctd = GenerateMetadataProxy(groups, groupList);
                // ns.Types.Add(ctd);
                groupList.Add(group.id, group.name);
            }

            // Generate all the SearchExpressions
            foreach (var searchexpression in metadata.searchExpressions)
            {
                CodeTypeDeclaration ctd = GenerateMetadataProxy(searchexpression, groupList);
                ns.Types.Add(ctd);
            }

            provider = CodeDomProvider.CreateProvider("CS");
        }

        // Generate Class for each Group
        private static CodeTypeDeclaration GenerateMetadataProxy(
            SearchMetadataGroup group,
            SortedDictionary<byte, string> groupList)
        {

            //TODO : Affecter les valeurs de "group" dans la classe ?!
            groupList.Add(group.id, group.name);

            var ctd = new CodeTypeDeclaration(
                string.Format("GeneratedGroups{0:N}", Guid.NewGuid()));

            var cmf = new CodeMemberField(
                GetType(String.Format("integer")),
                "id"
                );
            ctd.Members.Add(cmf);

            var cmf2 = new CodeMemberField(
                GetType(String.Format("string")),
                "name"
                );
            ctd.Members.Add(cmf2);

            // public <Type> FieldXXXX {
            var cmp = new CodeMemberProperty()
            {
                Name = String.Format("Id"),
                Type = GetType(String.Format("integer")),
                Attributes = MemberAttributes.Public | MemberAttributes.Final,
                HasGet = true,
                HasSet = true,
            };

            // public <Type> FieldXXXX {
            var cmp2 = new CodeMemberProperty()
            {
                Name = String.Format("Name"),
                Type = GetType(String.Format("string")),
                Attributes = MemberAttributes.Public | MemberAttributes.Final,
                HasGet = true,
                HasSet = true,
            };

            GenerateAssessors(cmp, cmf);
            GenerateAssessors(cmp2, cmf2);

            ctd.Members.Add(cmp);
            ctd.Members.Add(cmp2);

            return ctd;
        }

        private static void GenerateClone(CodeTypeDeclaration ctd)
        {
            // public ctor;
            ctd.Members.Add(
                new CodeConstructor()
                {
                    Attributes = MemberAttributes.Public
                });

            // internal ctor(x);
            var copyCtor = new CodeConstructor()
            {
                Attributes = MemberAttributes.FamilyAndAssembly
            };
            copyCtor.Parameters.Add(new CodeParameterDeclarationExpression(ctd.Name, "other"));

            foreach (var field in ctd.Members.OfType<CodeMemberField>())
            {
                // this.field = other.fied
                copyCtor.Statements.Add(
                    new CodeAssignStatement(
                            // this.field
                            new CodeFieldReferenceExpression(
                                new CodeThisReferenceExpression(),
                                field.Name),
                            // other.field    
                            new CodeFieldReferenceExpression(
                                new CodeArgumentReferenceExpression("other"),
                                field.Name)));
                        
            }
            ctd.Members.Add(copyCtor);

            // public object Clone(x);
            var cmm = new CodeMemberMethod()
            {
                Attributes = MemberAttributes.Final | MemberAttributes.Public,
                Name = "Clone",
                ReturnType = new CodeTypeReference(typeof(object)),
            };

            // return new X(this)
            cmm.Statements.Add(
                new CodeMethodReturnStatement(
                    new CodeObjectCreateExpression(
                    ctd.Name,
                    new CodeThisReferenceExpression())));

            ctd.Members.Add(cmm);
        }

        // <metadata>
        private static void GenerateProperty(CodeTypeDeclaration ownerClass, SearchMetadataSearchExpressionAdd add)
        {
            var count = ownerClass.Members.Count;

            var cmf = new CodeMemberField(GetType("string"), String.Format("metadata{0}", count));
            ownerClass.Members.Add(cmf);

            // public <Type> FieldXXXX {
            var cmp = new CodeMemberProperty()
            {
                Name = String.Format("Metadata{0}", count),
                Type = GetType("string"),
                Attributes = MemberAttributes.Public | MemberAttributes.Final,
                HasGet = true,
                HasSet = true,
            };

            GenerateAssessors(cmp, cmf);

            ownerClass.Members.Add(cmp);
        }

        // <appliesTo>
        private static void GenerateProperty(CodeTypeDeclaration ownerClass, SearchMetadataSearchExpressionAppliesToAdd add)
        {
            //TODO Is it a property ? Or does it only filters the metadata.

        }

        private static void GenerateProperty(CodeTypeDeclaration ownerClass, SearchMetadataSearchExpressionField field, int length)
        {
            var count = ownerClass.Members.Count;

            var cmf = new CodeMemberField(GetType(field.type), String.Format("field{0}", count));
            ownerClass.Members.Add(cmf);

            // public <Type> FieldXXXX {
            var cmp = new CodeMemberProperty()
            {
                Name = String.Format("Field{0}", count),
                Type = GetType(field.type),
                Attributes = MemberAttributes.Public | MemberAttributes.Final,
                HasGet = true,
                HasSet = true,
            };

            GenerateAssessors(cmp, cmf);

            GenerateSearchCriteriaAttribute(cmp, field.key, field.label, length);

            ownerClass.Members.Add(cmp);
        }

        private static void GenerateAssessors(CodeMemberProperty cmp, CodeMemberField cmf)
        {
            /*
            get
            {
                return this.field;
            }
            */
            cmp.GetStatements.Add(new CodeMethodReturnStatement(
                    new CodeFieldReferenceExpression(
                        new CodeThisReferenceExpression(), cmf.Name)
                ));

            /*
            set
            {
                this.field = value;
            }
             */
            cmp.SetStatements.Add(new CodeAssignStatement(
                    new CodeFieldReferenceExpression(
                        new CodeThisReferenceExpression(), cmf.Name),
                    new CodeArgumentReferenceExpression("value")));
        }

        private static void GenerateSearchCriteriaAttribute(CodeMemberProperty cmp, string key, string value, int length = 1)
        {
            var type = cmp.Type.BaseType;

            string consideredComponent;

            switch (type.ToString())
            {
                case "System.Boolean":
                    consideredComponent = "CheckBox";

                    break;
                case "System.DateTime":
                    consideredComponent = "DatePicker";
                    break;
                case "System.String":
                case "System.Integer":
                case "System.Single":
                case "System.Float":
                    consideredComponent = "TextBox";
                    break;
                default:
                    //TODO
                    consideredComponent = "TextBox";
                    break;
            }

            // [SearchCriteria("<Key>", "<Value>", "<ComponentType>")]
            cmp.CustomAttributes.Add(new CodeAttributeDeclaration(
                new CodeTypeReference(typeof(SearchCriteriaAttribute)),
                new CodeAttributeArgument(
                    new CodePrimitiveExpression(key)),
                new CodeAttributeArgument(
                    new CodePrimitiveExpression(value)),
                new CodeAttributeArgument(
                    new CodePrimitiveExpression(consideredComponent))
                ));
        }

        private static CodeTypeReference GetType(string p)
        {
            //TODO : Switch Case avec valeur du XML
            switch (p.ToLower())
            {
                case "integer":
                    return new CodeTypeReference(typeof(int));
                case "decimal":
                case "float":
                    return new CodeTypeReference(typeof(float));
                case "string":
                    return new CodeTypeReference(typeof(string));
                case "binary":
                    return new CodeTypeReference(typeof(bool));
                case "multiple":
                    return new CodeTypeReference(typeof(bool));
                case "date":
                    return new CodeTypeReference(typeof(DateTime));
                default:
                    // Extended cases
                    return new CodeTypeReference(typeof(Control));
            }


        }

        #region Deprecated

        // Generate Class for each SearchExpression
        private static CodeTypeDeclaration GenerateMetadataProxy(
            SearchMetadataSearchExpression searchexpression,
            SortedDictionary<byte, string> groupList)
        {
            // public class GeneratedSearchExpressionXXXXXX.... : ISearchExpression
            var ctd = new CodeTypeDeclaration(
                string.Format("GeneratedSearchExpression{0:N}", Guid.NewGuid()));

            ctd.BaseTypes.Add(new CodeTypeReference(typeof(ISearchExpression)));

            ctd.Attributes = MemberAttributes.Public | MemberAttributes.Final;

            String groupName;
            groupList.TryGetValue(searchexpression.groups.groupref.id, out groupName);
            // TODO catch (KeyNotFoundException e) If the XML file contains a wrong id number

            // [GroupAttribute("GroupName")]
            ctd.CustomAttributes.Add(
                new CodeAttributeDeclaration(new CodeTypeReference(typeof(GroupAttribute)),
                new CodeAttributeArgument(new CodePrimitiveExpression(groupName))
            ));
            // [SearchExpressionAttribute]
            ctd.CustomAttributes.Add(
                new CodeAttributeDeclaration(new CodeTypeReference(typeof(SearchExpressionAttribute))
            ));

            // Generate Metadata Properties
            foreach (var add in searchexpression.metadata)
            {
                GenerateProperty(ctd, add);
            }

            //TODO AppliesTo.Remove
            // Generate AppliesTo.add Properties
            foreach (var action in searchexpression.appliesTo.add)
            {
                GenerateProperty(ctd, action);
            }

            // Generate Fields Properties
            foreach (var field in searchexpression.fields)
            {
                GenerateProperty(ctd, field, searchexpression.fields.Length);
            }

            GenerateClone(ctd);

            return ctd;
        }

        #endregion

    }
}
